//
//  CKLSoundStretch.m
//  SoundDemo
//
//  Created by wankang on 1/23/15.
//  Copyright (c) 2015 wankang. All rights reserved.
//

#import "CKLSoundStretch.h"
#import "audio2wav.h"

typedef const char *LPCSTR;
#define FLAG_MUSIC      0x00
#define FLAG_SPEECH     0x01
#define FLAG_QUICK      0x02
#define FLAG_DETECTBPM  0x04
#define FLAG_NOANTIALIAS    0x08

int soundstretch_proc(LPCSTR szFileIn, LPCSTR szFileOut, const float *delta3, int nOption);
int soundstretch_fromData(const unsigned char *pbData, int nData, LPCSTR szFileOut, const float *delta3, int nOption);
typedef void (*LPWaveCallback)(void *lpContext, int nCase, void *lpData, int nBytes);
int createStretchedWave(LPCSTR szWaveInFile, LPWaveCallback lpfnCallback, void *lpContext, float fDelta[3], int nOption);
int createStretchedWave_fromData(const unsigned char *pbData, int nData, LPWaveCallback lpfnCallback, void *lpContext, float fDelta[3], int nOption);

//////////////////////////////////////////////////////////////////////////////////////

typedef struct tagWaveData {
    wavhdr *m_lpData;
    int m_nData;
    int m_nAlloc;
    int m_nChannel;
    int m_nSampleRate;
}SWaveData;

void saveMp3WaveData(void *lpContext, int nType, const unsigned char *lpData, int nBytes) {
    SWaveData *p = (SWaveData *)lpContext;
    switch (nType) {
        case 1:
            if (lpData != NULL && nBytes == sizeof(int)*2) {
                const int *pn = (const int *)lpData;
                p->m_nChannel = pn[0];
                p->m_nSampleRate = pn[1];
                int nSeconds = 5;  // initial buffer size: 5 seconds
                p->m_nAlloc = sizeof(wavhdr) + sizeof(short)*p->m_nChannel*p->m_nSampleRate*nSeconds;
                p->m_lpData = (wavhdr *)malloc(p->m_nAlloc);
                if (p->m_lpData != NULL)
                    p->m_nData = (int)sizeof(wavhdr);
            }
            break;
        case 2:
            if (p->m_lpData != NULL) {
                // to check buffer size
                int nMaxBytes = p->m_nData + nBytes;
                if (nMaxBytes > p->m_nAlloc) {
                    int nBaseAlloc = sizeof(short)*p->m_nChannel*p->m_nSampleRate;
                    int nSeconds = (nMaxBytes - sizeof(wavhdr) + nBaseAlloc - 1)/nBaseAlloc;
                    int nAlloc = sizeof(wavhdr) + nBaseAlloc * nSeconds;
                    void *pNew = realloc(p->m_lpData, nAlloc);
                    if (!pNew)
                        return;
                    p->m_lpData = (wavhdr *)pNew;
                    p->m_nAlloc = nAlloc;
                }
                // to save data
                char *q = (char *)p->m_lpData;
                memcpy(q+p->m_nData, lpData, nBytes);
                p->m_nData += nBytes;
            }
            break;
        case 4:
            if (p->m_lpData != NULL) {
                int nWaveBytes = p->m_nData - (int)sizeof(wavhdr);
                wavhdr *wavh = p->m_lpData;
                memset(wavh, 0, sizeof(wavhdr));
                wavh->nChannels = (short)p->m_nChannel;
                wavh->nSamplesPerSec = p->m_nSampleRate;
                wavh->RIFF = 0x46464952;	// 'RIFF'
                wavh->size = (int)(nWaveBytes + sizeof(wavhdr) - 8);	// total size of wave file, including header
                wavh->WAVE = 0x45564157;	// 'WAVE'
                wavh->fmt = 0x20746D66;	// 'fmt '
                wavh->wBitsPerSample = 16;
                wavh->wFormatLength = 16;
                wavh->wFormatTag = 1;
                wavh->data = 0x61746164;	// 'data'
                wavh->dSize = (int)nWaveBytes;
                wavh->nBlockAlign = (short)(wavh->wBitsPerSample / 8 * wavh->nChannels);
                wavh->nAvgBytesPerSec = wavh->nSamplesPerSec * wavh->nBlockAlign;
            }
            break;
        default:
            break;
    }
}

typedef struct tagSavePcmContext
{
    __unsafe_unretained id<WaveOutDelegate> delegate;
}SavePcmContext;

void savePcmData(void *lpContext, int nType, void *lpData, int nData)
{
    SavePcmContext *p = (SavePcmContext *)lpContext;
    switch(nType) {
        case 0:
            if (lpData!=NULL && nData>=sizeof(int)*3) {
                const int *pn = (const int *)lpData;
                if (p->delegate && [p->delegate respondsToSelector:@selector(initSampleRate:bits:channel:)]) {
                    [p->delegate initSampleRate:pn[0] bits:pn[1] channel:pn[2]];
                }
            }
            break;
        case 1:
        if (p->delegate && [p->delegate respondsToSelector:@selector(waveOutSample:dataLength:)]) {
            [p->delegate waveOutSample:(const void *)lpData dataLength:nData];
        }
            break;
        case 2:
        if (p->delegate && [p->delegate respondsToSelector:@selector(endWaveOut)]) {
            [p->delegate endWaveOut];
        }
            break;
        default:
            break;
    }
}
//////////////////////////////////////////////////////////////////////////////////////

@implementation CKLSoundStretch
@synthesize strFileIn = _strFileIn;
@synthesize strFileOut = _strFileOut;
@synthesize strTmpFolder = _strTmpFolder;
@synthesize delegate = _delegate;

-init
{
    if ((self = [super init]) != nil) {
        _strFileIn = nil;
        _strFileOut = nil;
        _strTmpFolder = nil;
        _delegate = nil;
    }
    return self;
}

-initWithInFile:(NSString *)sInFile andOutFile:(NSString *)sOutFile
{
    if ((self = [super init]) != nil) {
        _strFileIn = sInFile;
        _strFileOut = sOutFile;
        _strTmpFolder = nil;
        _delegate = nil;
    }
    return self;
}

-(void)dealloc
{
    [self close];
}

-(void)close
{
    _strFileIn = nil;
    _strFileOut = nil;
    _strTmpFolder = nil;
    _delegate = nil;
}

-(BOOL)convertWithTempoDelta:(NSInteger)tempo pitchDelta:(NSInteger)pitch rateDelta:(NSInteger)rate
                  inFastMode:(BOOL)fFast forSpeech:(BOOL)fSpeech
{
    if (tempo<-95 || tempo>5000 || pitch<-60 || pitch>60 || rate<-95 || rate>5000)
        return NO;
    if (_strFileIn==nil || _strFileIn.length==0 ||
        (_delegate==nil && (_strFileOut==nil || _strFileOut.length==0)))
        return NO;
    // if it is in .mp3 format, convert to .wav firstly
    NSData *pData = nil;
    NSString *sInFile = _strFileIn;
    if ([[sInFile pathExtension] caseInsensitiveCompare:@"mp3"] == NSOrderedSame
        || [[sInFile pathExtension] caseInsensitiveCompare:@"m"] == NSOrderedSame) {
        if (_strTmpFolder==nil || _strTmpFolder.length==0)
            return NO;
        sInFile = [self getTmpWaveFileNameFromMpegFile:sInFile];
        if (sInFile==nil || sInFile.length==0)
            return NO;
        pData = [CKLSoundStretch getWaveDataFromMp3File:_strFileIn];
        if (pData == nil)
            return NO;
    }
    // prepare to call soundstretch_proc
    float fDelta[3] = {tempo, pitch, rate};
    int nOption = FLAG_QUICK;
    if (fFast)
        nOption |= FLAG_QUICK;
    if (fSpeech)
        nOption |= FLAG_SPEECH;
    int nRes;
    if (_delegate != nil) {
        SavePcmContext context;
        // to create strong reference
        id<WaveOutDelegate> callProc = self.delegate;
        context.delegate = callProc;
        if (pData != nil)
            nRes = createStretchedWave_fromData(pData.bytes, (int)pData.length, savePcmData, &context, fDelta, nOption);
        else
            nRes = createStretchedWave(sInFile.UTF8String, savePcmData, &context, fDelta, nOption);
        callProc = nil;
    } else {
        if (pData != nil)
            nRes = soundstretch_fromData(pData.bytes, (int)pData.length, _strFileOut.UTF8String, fDelta, nOption);
        else
            nRes = soundstretch_proc(sInFile.UTF8String, _strFileOut.UTF8String, fDelta, nOption);
    }
    pData = nil;
    return (nRes == 0);
}

-(NSString *)getTmpWaveFileNameFromMpegFile:(NSString *)sFile
{
    if (_strTmpFolder==nil || _strTmpFolder.length==0)
        return nil;
    NSString *sFileName;
    NSRange r = [sFile rangeOfString:@"/" options:NSBackwardsSearch];
    if (r.length == 0)
        sFileName = sFile;
    else
        sFileName = [sFile substringFromIndex:r.location+1];
    // it should be a .mp3 file
    NSString *sFullName;
    NSString *sName = [sFileName stringByDeletingPathExtension];
    if ([_strTmpFolder hasSuffix:@"/"])
        sFullName = [_strTmpFolder stringByAppendingFormat:@"%@.wav",sName];
    else
        sFullName = [_strTmpFolder stringByAppendingFormat:@"/%@.wav",sName];
    return sFullName;
}

-(BOOL)makeSpeechInWaveFasterOrSlower:(NSInteger)tempo
{
    if (tempo == 0)
        return NO;
    return [self convertWithTempoDelta:tempo pitchDelta:0 rateDelta:0 inFastMode:NO forSpeech:YES];
}

+(NSString *)convertMp3File:(NSString *)sInFile toFile:(NSString *)sOutFile inWaveFormat:(BOOL)fWaveFile
{
    if (sInFile==nil || sInFile.length<=0 || sOutFile==nil || sOutFile.length<=0)
        return @"invalid argument";
    char szBuf[300];
    szBuf[0] = '\0';
    LPCSTR pszIn = sInFile.UTF8String;
    int nRes = MpegDecoder(pszIn, sOutFile.UTF8String, (fWaveFile ? 1 : 0), szBuf, 300, NULL);
    if (nRes != 0) {
        if (szBuf[0] != '\0')
            snprintf(szBuf, 300, "error = %d", nRes);
        return [NSString stringWithUTF8String:szBuf];
    }
    return nil;
}

+(NSData *)getWaveDataFromMp3File:(NSString *)sInFile
{
    if (sInFile==nil || sInFile.length<=0)
        return nil;
    char szBuf[300];
    szBuf[0] = '\0';
    SWaveData saveContext;
    saveContext.m_lpData = NULL;
    int nRes = MpegDecodeWithCallback(sInFile.UTF8String, szBuf, 300, saveMp3WaveData, &saveContext);
    if (nRes != 0) {
        free(saveContext.m_lpData);
        return nil;
    }
    return [NSData dataWithBytesNoCopy:saveContext.m_lpData length:saveContext.m_nData];
}
@end
